web/src/routes/image/[id]/+page.svelte (view raw)
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { getImage, imageDownloadUrl, thumbnailUrl, type Image } from '$lib/api';
4
5 let { params } = $props();
6 let id = $derived(parseInt(params.id));
7
8 let img = $state<Image | null>(null);
9 let loading = $state(true);
10 let error = $state('');
11
12 onMount(async () => {
13 try {
14 img = await getImage(id);
15 } catch (err) {
16 error = err instanceof Error ? err.message : 'Failed to load image';
17 } finally {
18 loading = false;
19 }
20 });
21
22 function formatSize(bytes: number): string {
23 if (bytes < 1024) return bytes + ' B';
24 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
25 return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
26 }
27
28 function formatDate(s: string): string {
29 return new Date(s).toLocaleString();
30 }
31</script>
32
33{#if loading}
34 <p>Loading...</p>
35{:else if error}
36 <p class="error">{error}</p>
37{:else if img}
38 <h1>{img.filename}</h1>
39
40 <div class="detail">
41 <div class="preview">
42 {#if img.thumbnail_path}
43 <a href={imageDownloadUrl(img.id)} download={img.filename}>
44 <img src={thumbnailUrl(img.id)} alt={img.filename} />
45 </a>
46 {:else}
47 <div class="placeholder">No thumbnail</div>
48 {/if}
49 </div>
50
51 <div class="meta">
52 <dl>
53 <dt>Dimensions</dt>
54 <dd>{img.width} × {img.height}</dd>
55 <dt>File size</dt>
56 <dd>{formatSize(img.file_size)}</dd>
57 <dt>Type</dt>
58 <dd>{img.mime_type}</dd>
59 <dt>Tags</dt>
60 <dd>
61 {#if img.tags.length > 0}
62 {img.tags.join(', ')}
63 {:else}
64 <span class="dim">none</span>
65 {/if}
66 </dd>
67 <dt>Uploaded</dt>
68 <dd>{formatDate(img.created_at)}</dd>
69 </dl>
70
71 <a href={imageDownloadUrl(img.id)} class="download" download={img.filename}>
72 Download original
73 </a>
74 </div>
75 </div>
76
77 <p class="back"><a href="/browse">← Back to browse</a></p>
78{/if}
79
80<style>
81 .detail {
82 display: flex;
83 gap: 2rem;
84 margin-top: 1rem;
85 flex-wrap: wrap;
86 }
87 .preview img {
88 max-width: 100%;
89 max-height: 60vh;
90 border-radius: 8px;
91 box-shadow: 0 2px 8px rgba(0,0,0,0.15);
92 }
93 .placeholder {
94 width: 300px;
95 height: 200px;
96 display: flex;
97 align-items: center;
98 justify-content: center;
99 background: #f0f0f0;
100 border-radius: 8px;
101 color: #999;
102 }
103 .meta {
104 min-width: 220px;
105 }
106 dl {
107 margin: 0;
108 }
109 dt {
110 font-weight: 600;
111 margin-top: 0.5rem;
112 font-size: 0.85rem;
113 color: #555;
114 }
115 dd {
116 margin: 0;
117 font-size: 0.95rem;
118 }
119 .dim {
120 color: #999;
121 }
122 .download {
123 display: inline-block;
124 margin-top: 1.5rem;
125 padding: 0.5rem 1.5rem;
126 background: #1a1a2e;
127 color: #eee;
128 text-decoration: none;
129 border-radius: 6px;
130 }
131 .download:hover {
132 background: #16213e;
133 }
134 .back {
135 margin-top: 2rem;
136 }
137 .error {
138 color: #c0392b;
139 }
140</style>